package com.airbnb.mvrx.mocking

import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.extensionReceiverParameter
import kotlin.reflect.full.functions
import kotlin.reflect.full.instanceParameter
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.jvmName

internal val KClass<*>.isEnum: Boolean
    get() {
        return getIfReflectionSupported {
            this::class.java.isEnum || isSubclassOf(Enum::class)
        } == true
    }

internal val KClass<*>.isKotlinClass: Boolean
    get() {
        return this.java.declaredAnnotations.any {
            it.annotationClass.qualifiedName == "kotlin.Metadata"
        }
    }

internal val KClass<*>.isObjectInstance: Boolean
    get() {
        return getIfReflectionSupported {
            objectInstance
        } != null
    }

/**
 * Some objects cannot be access with Kotlin reflection, and throw UnsupportedOperationException.
 *
 * The error message is:
 * "This class is an internal synthetic class generated by the Kotlin compiler.
 * It's not a Kotlin class or interface, so the reflection library has no idea what declarations does it have."
 */
internal fun <T> getIfReflectionSupported(block: () -> T): T? {
    return try {
        block()
    } catch (e: UnsupportedOperationException) {
        null
    }
}

@Suppress("UNCHECKED_CAST")
internal fun <T : Any> KClass<T>.copyMethod(): KFunction<T> =
    this.memberFunctions.first { it.name == "copy" } as KFunction<T>

/** Call the copy function of the Data Class receiver. The params are a map of parameter name to value. */
internal fun <T : Any> T.callCopy(vararg params: Pair<String, Any?>): T {
    val paramMap = params.associate { it }
    return this::class.copyMethod().callNamed(paramMap, self = this)
}

/**
 * Invoke a function with the given parameter names.
 *
 * @param params Mapping of parameter name to parameter value
 * @param self The receiver of the function
 * @param extSelf The extension receiver of the function
 */
internal fun <R> KFunction<R>.callNamed(
    params: Map<String, Any?>,
    self: Any? = null,
    extSelf: Any? = null
): R {
    val map = params.mapTo(ArrayList()) { (key, value) ->
        val param = parameters.firstOrNull { it.name == key }
            ?: error("No parameter named '$key' found on copy function for '${this.returnType.classifier}'")
        param to value
    }

    if (self != null) map += instanceParameter!! to self
    if (extSelf != null) map += extensionReceiverParameter!! to extSelf
    return callBy(map.toMap())
}

/** Helper to call a function reflectively. */
internal inline fun <reified T> Any.call(functionName: String, vararg args: Any?): T {
    return findFunction(functionName).call(this, *args) as T
}

internal fun Any.findFunction(functionName: String): KFunction<*> {
    return this::class.findFunction(functionName)
}

internal fun KClass<*>.findFunction(functionName: String): KFunction<*> {
    return functions.find { it.name == functionName }
        ?: error("No function found with name $functionName in class $simpleName")
}

/**
 * Ensures that the given class is:
 * - A kotlin data class
 * - Contains no mutable properties
 * - Contains no primary constructor params with a Mutable collection type.
 *
 * For further validations restricting usages of Java collections such as ArrayList, this can be combined with [com.airbnb.mvrx.assertMavericksDataClassImmutability]
 */
fun assertMavericksDataClassImmutabilityWithKotlinReflect(
    kClass: KClass<*>,
) {
    require(kClass.isData) {
        "Mavericks state must be a data class! - ${kClass.jvmName}"
    }

    kClass.primaryConstructor?.parameters?.forEach { param ->
        val typeString = param.type.toString()

        when {
            // Note, theoretically typeOf<MutableList>() should work (https://youtrack.jetbrains.com/issue/KT-35877)
            // but Android Studio can't resolve that function so I can't get that approach to work. (maybe related to https://youtrack.jetbrains.com/issue/KTIJ-20328)
            "MutableList" in typeString -> {
                "You cannot use MutableList for ${param.name}. Use the read only List instead."
            }
            "MutableMap" in typeString -> {
                "You cannot use MutableMap for ${param.name}. Use the read only Map instead."
            }
            "MutableSet" in typeString -> {
                "You cannot use MutableSet for ${param.name}. Use the read only Set instead."
            }
            else -> null
        }?.let { throw IllegalArgumentException("Invalid property in state ${kClass.jvmName}: $it") }
    }

    kClass.declaredMemberProperties.forEach { prop ->
        require(prop !is KMutableProperty<*>) {
            "Mutable property ${prop.name} is not allowed in state ${kClass.jvmName}"
        }
    }
}
